package org.ourguy.mymoviesandshows.managers;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;

import org.ourguy.mymoviesandshows.R.string;
import org.ourguy.mymoviesandshows.apiclient.APIException;
import org.ourguy.mymoviesandshows.apiclient.IMixedAPIClient;
import org.ourguy.mymoviesandshows.apiclient.trakt.TraktClient;
import org.ourguy.mymoviesandshows.datamodel.Episode;
import org.ourguy.mymoviesandshows.datamodel.ITitle;
import org.ourguy.mymoviesandshows.datamodel.Movie;
import org.ourguy.mymoviesandshows.datamodel.Season;
import org.ourguy.mymoviesandshows.datamodel.Series;

import android.content.Context;

//TODO make this connectivity-aware (refresh methods)
public class DataAccessManager {
	
	/*
	 * File names of the local title cache
	 */
	private static final String TITLE_CACHE = "title_cache.ser";
	private static final String TRENDING_CACHE = "trending_cache.ser";
	
	private int age;
	
	/**
	 * The singleton instance of this object
	 */
	private static DataAccessManager instance;
	
	/**
	 * The object providing connectivity to the API.
	 */
	private IMixedAPIClient apiConnection;
	
	/**
	 * Our cache of titles
	 */
	private Hashtable<String, ITitle> titleCache; //hashtable is thread-safe
	
	/**
	 * Our cache of the trending titles
	 */
	//TODO make this thread safe
	private volatile List<String> trendingTitles;
	
	/**
	 * Get the singleton instance of this object
	 * @param context The context we are instantiating the DAM from
	 * @return The instance of DAM
	 */
	public static synchronized DataAccessManager getInstance(Context context){
		if (instance == null)
			instance = new DataAccessManager(context);
		return instance;
	}
	
	/**
	 * Instantiate a new DAM
	 * @param context
	 */
	private DataAccessManager(Context context){
		apiConnection = new TraktClient();
		try {
			deserializeShows(context);
		} catch (DataAccessException ex){}
		age = 0;
	}
	
	/**
	 * Check if a TV show has new episodes.
	 * @param series
	 */
	//TODO this method ought to check just the summary of the series
	private boolean isValid(ITitle title) {
		if(title instanceof Series) {
			/*Series series = (Series) title;
			try {
				List<SeasonInfo> remote = 
						apiConnection.getSeasonInfo(series.getApiId());
	 			List<Season> local = series.getSeasonList();
				
				//Check if the number of seasons is the same
				if (local == null || remote.size() != local.size())
					return false;
				//Check if the number of episodes in the last season is the same
				else if (Collections.max(remote).getEpisodeCount() !=
						Collections.max(local).numEpisodes())
					return false;
				else //If they are, we're OK
					return true;*/
				return true;
			/*} catch (APIException e) {
				//TODO add connectivity check here, return true only if no conn
				//if (e.getCause() instanceof HTTPClientException)
				return true;
			}*/
		} else { // We only know how to check series for now...
			return false;
		}
	}
	
	// Proxy methods
	/**
	 * Get an autocomplete-friendly list of movies from the API
	 * @param q the query
	 * @return the list of titles matching the query
	 * @throws DataAccessException upon APIException
	 */
	public Hashtable<String, String> autoCompleteMovie(String q)
			throws DataAccessException {
		try {
			return apiConnection.autoCompleteMovie(q);
		} catch (APIException e) {
			throw new DataAccessException();
		}
	}
	
	/**
	 * Get an autocomplete-friendly list of series from the API
	 * @param q the query
	 * @return the list of titles matching the query
	 * @throws DataAccessException upon APIException
	 */
	public Hashtable<String, String> autoCompleteSeries(String q)
			throws DataAccessException {
		try {
			return apiConnection.autoCompleteSeries(q);
		} catch (APIException e) {
			throw new DataAccessException();
		}
	}
	
	/**
	 * Get an autocomplete-friendly list of titles from the API
	 * @param q the query
	 * @return the list of titles matching the query
	 * @throws DataAccessException upon APIException
	 */
	public Hashtable<String, String> autoCompleteTitle(String q)
			throws DataAccessException {
		try {
			return apiConnection.autoCompleteTitle(q);
		} catch (APIException e) {
			throw new DataAccessException();
		}
	}
	
	/**
	 * Search the API for a movie
	 * @param q the query
	 * @return the list of titles matching the query
	 * @throws DataAccessException upon APIException
	 */
	//TODO think this through
	public ArrayList<Movie> searchMovies(String q)
			throws DataAccessException {
		ArrayList<Movie> results = new ArrayList<Movie>();
		try {
			ArrayList<Movie> apiResults = apiConnection.searchMovies(q);
			Iterator<Movie> i = apiResults.iterator();
			while(i.hasNext()) {
				Movie remote = i.next();
				Movie local = (Movie) titleCache.get(remote.getApiId());
				if(local != null && isValid(local)){
					results.add(local);
				} else {
					results.add(remote);
				}
			}
		} catch (APIException e) {
			throw new DataAccessException();
		}
		return results;
	}
	
	/**
	 * Search the API for a TV show
	 * @param q the query
	 * @return the list of titles matching the query
	 * @throws DataAccessException upon APIException
	 */
	//TODO think this through
	public ArrayList<ITitle> searchSeries(String q)
			throws DataAccessException {
		ArrayList<ITitle> results = new ArrayList<ITitle>();
		try {
			ArrayList<ITitle> apiResults = apiConnection.searchSeries(q);
			Iterator<ITitle> i = apiResults.iterator();
			while(i.hasNext()) {
				ITitle remote = i.next();
				ITitle local = titleCache.get(remote.getApiId());
				if(local != null && isValid(local)){
					results.add(local);
				} else {
					results.add(remote);
				}
			}
		} catch (APIException e) {
			throw new DataAccessException();
		}
		return results;
	}
	
	/**
	 * Search the API for a title
	 * @param q the query
	 * @return the list of titles matching the query
	 * @throws DataAccessException upon APIException
	 */
	//TODO think this through
	/*public ArrayList<ITitle> searchTitles(String q)
			throws DataAccessException {
		try {
			return apiConnection.searchTitles(q);
		} catch (APIException e) {
			throw new DataAccessException();
		}
	}*/
	
	// End of proxy methods
	
	/**
	 * Refresh the cache of trending shows
	 * @throws DataAccessException
	 */
	private void refreshTrendingList() throws DataAccessException{
		try {
			ArrayList<ITitle> trends = apiConnection.getTrendingSeries();
			trendingTitles = new ArrayList<String>();
			Iterator<ITitle> i = trends.iterator();
			while(i.hasNext())
				trendingTitles.add(i.next().getApiId());
		} catch (APIException e) {
			throw new DataAccessException("Connection error");
		}
	}

 	public ArrayList<Season> getSeasonsForTitle(ITitle title)
 			throws DataAccessException{
 		try {
			return apiConnection.getSeasons(title.getApiId());
		} catch (APIException e) {
			throw new DataAccessException("Connection error");
		}
 	}
 	
 	public Season getSeasonAtIndex(ITitle title, int index)
 			throws DataAccessException{
 		try {
			return apiConnection.getSeason(title.getApiId(), index);
		} catch (APIException e) {
			throw new DataAccessException("Connection error");
		} 
 	}
 	
 	//TODO this method must call getTitle, then getSeason and then check to see 
 	//if the episode exists, in order to facilitate proper caching.
 	public Episode getEpisode(String id, int seasonNumber, int episodeNumber)
 			throws DataAccessException {
 		Episode localHit = null;
 		Episode remoteHit = null;
 		
 		try {
 			remoteHit =
 					apiConnection.getEpisode(id, seasonNumber, episodeNumber);
 		} catch (APIException ignored) {}
 		
 		if (titleCache.containsKey(id)) { //This must be replaced by getTitle
 			Series localTitle = (Series)titleCache.get(id);
 			Season localSeason = localTitle.getSeasonAt(seasonNumber);
 			if(localSeason != null) { // This must be replaced by getSeason
 				localHit = localSeason.getEpisodeAt(episodeNumber);//.updateWith(remoteHit);
 			}
 		}
 		if(localHit != null) {
 			return localHit;
 		} else if (remoteHit != null) {
 			return remoteHit;
 		} else {
 			throw new DataAccessException("Could not fetch the requested" +
 					" episode");
 		}
 	}
	
	public void addToFavorites(ITitle favTitle, Context context){
		if(!titleCache.contains(favTitle.getApiId())) {
			titleCache.put(favTitle.getApiId(), favTitle);
		}
		titleCache.get(favTitle.getApiId()).addToFavorites();
		age++;
		try {
			saveState(context);
		} catch (DataAccessException e) {}
	}
	
	public void removeFromFavorites(ITitle favTitle, Context context){
		if(!titleCache.contains(favTitle.getApiId())) {
			titleCache.put(favTitle.getApiId(), favTitle);
		}
		titleCache.get(favTitle.getApiId()).removeFromFavorites();
		age++;
		try {
			saveState(context);
		} catch (DataAccessException e) {}
	}
	
	public ArrayList<ITitle> getTrending() throws DataAccessException{
		List<String> oldTrendings = trendingTitles;
			try {
				refreshTrendingList();
			} catch (DataAccessException e) {
				trendingTitles = oldTrendings;
				throw new DataAccessException("Connection error");
			}
		
		ArrayList<ITitle> result = new ArrayList<ITitle>();
		Iterator<String> i = trendingTitles.iterator();
		while(i.hasNext())
			result.add(getTitle(i.next()));
		
		return result;
	}
	
	public ITitle getTitle(String id) {
		ITitle title = titleCache.get(id);
		if (title == null || !isValid(title))
			try {
				title=apiConnection.getTitleSummary(id);
				titleCache.put(id, title);
			} catch (APIException e) {}

		return title;
	}

	public ArrayList<ITitle> getFavorites(){
		ArrayList<ITitle> result = new ArrayList<ITitle>();
		Iterator<ITitle> i = titleCache.values().iterator();
		while(i.hasNext()) {
			ITitle title = i.next();
			if(title.isInFavorites())
				result.add(title);
		}
		return result;
	}
	
	public int getFavouritesAge() {
		return age;
	}
	public ArrayList<Episode> getUpComing(Series title) throws DataAccessException {
		ArrayList<Episode> upcomingList = new ArrayList<Episode>();
		try {
			upcomingList.addAll(apiConnection.getFutureEpisodeList(title.getApiId()));
		} catch (APIException ex){
			throw new DataAccessException();
		}
		return upcomingList;
	}
	
	public ArrayList<Episode> getUpComing(ArrayList<ITitle> titles) 
			throws DataAccessException{
		ArrayList<Episode> upcomingList = new ArrayList<Episode>();
		try {
			for (int i = 0; i < titles.size(); i++){
				upcomingList.addAll(apiConnection.getFutureEpisodeList(titles.get(i).getApiId()));
			}
		} catch (APIException ex){
			throw new DataAccessException();
		}
		return upcomingList;
	}
	
	public ITitle getTitleAtIndex(ArrayList<ITitle> list, int index){
		if (list.size() > index && index >= 0)
			return list.get(index);
		return null;
	}
	
	public ITitle getTrendingAtIndex(int index){
		return getTitle(trendingTitles.get(index));
	}
	
	public void addSeasonsToShow(ITitle title)
	{
		new Thread(new AddSeasonsToShow((Series)title)).start();
	}
	
	private synchronized void deserializeShows(Context context) 
			throws DataAccessException{
		
		titleCache = new Hashtable<String, ITitle>();
		trendingTitles = new ArrayList<String>();
		Object o;
		ObjectInputStream in;
		
		try {
			in = new ObjectInputStream(context.openFileInput(TITLE_CACHE));
			try {
				o = in.readObject();
				titleCache = (Hashtable<String, ITitle>) o;
				for (Hashtable.Entry<String, ITitle> entry : titleCache.entrySet()) {
					if(((Series)entry.getValue()).isInFavorites())
						new Thread(new AddSeasonsToShow((Series)entry.getValue())).start();
				   }

			} catch (Exception e) {
				throw new DataAccessException("Serialization error during" +
						" read of " + TITLE_CACHE);
			} finally {
				in.close();
			}
		} catch(IOException e) {
			throw new DataAccessException("Serialization error during" +
					" open of " + TITLE_CACHE);
		}

		try {
			in = new ObjectInputStream(context.openFileInput(TRENDING_CACHE));
			try {
				o = in.readObject();
				trendingTitles = (List<String>) o;
			} catch (Exception e) {
				throw new DataAccessException("Serialization error during" +
						" read of " + TRENDING_CACHE);
			} finally {
				in.close();
			}
		} catch(IOException e) {
			throw new DataAccessException("Serialization error during" +
					" open of " + TRENDING_CACHE);
		}
	}
	
	public void saveState(Context context) throws DataAccessException{
		serializeShows(context);
	}
	
	private synchronized void serializeShows(Context context)
			throws DataAccessException{
		new Serializer(titleCache, TITLE_CACHE, context).start();
	}
	
	//TODO add a callback to check if we wrote the data successfully
	private class Serializer extends Thread {
		private String fileName;
		private Object obj;
		private Context context;
		
		public Serializer(Object obj, String fileName, 
				Context context){
			this.obj = obj;
			this.fileName = fileName;
			this.context = context;
		}
		
		public void run() {
			try{ 
				ObjectOutputStream out =
						new ObjectOutputStream(context
								.openFileOutput(fileName, 0));
				
				out.writeObject(obj);
				out.close();
			} catch (Exception ignored) { }
		}
	}
	
	private class AddSeasonsToShow implements Runnable{
		Series title;
		
		public AddSeasonsToShow(Series title) {
			this.title = title;
		}
		
		public void run() {
			try {
				if(title.getSeasonList() == null)
					title.setSeasons(getSeasonsForTitle(title));
			} catch (Exception e) {
			}
			int i=1;
		}
	}
}
